package com.beerandjesus.sportofkings.handicapper.dao.aspect;

import com.beerandjesus.sportofkings.handicapper.dao.ConnectionManager;
import com.beerandjesus.sportofkings.handicapper.dao.Database;
import org.apache.log4j.Logger;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Aspect;

import javax.sql.DataSource;
import java.sql.Connection;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Aspect for simple management of transactions for ConnectionManager database connections.
 *
 * This object MUST be provided with a map associating each Database.Datasource with its
 * associated DataSource reference.
 *
 * In addition, ConnectionManager's get*Connection methods must be intercepted using an
 * AOP mechanism such that this object's get*Connection methods are initiated appropriately.
 *
 * Date: Jul 25, 2012
 * Time: 5:03:18 PM
 */
@Aspect
public class TransactionAspect extends AbstractAnnotationAspect<TransactionBoundary> {

    private static final Logger LOG = Logger.getLogger(TransactionAspect.class);

    public Logger getLogger() {
        return LOG;
    }

    private ConnectionManager connectionManager;
    private Map<String, DataSource> dataSources;

    private ThreadLocal<Map<String, Connection>> connections = new ThreadLocal<Map<String, Connection>>() {
        @Override
        protected Map<String, Connection> initialValue() {
            return new HashMap<String, Connection>();
        }
    };


    /**
     * Apply transactional semantics to a method. The method
     * must be annotated with @TransactionBoundary specifying
     * the Database.Datasource that needs to be used.
     * @param pjp The join point
     * @throws Throwable When something goes wrong
     * @return The method's return object
     */
    public Object doInTransaction(ProceedingJoinPoint pjp) throws Throwable {
        TransactionBoundary anno = obtainAnnotation(pjp, TransactionBoundary.class);
        if (anno == null) {
            throw new RuntimeException("TransactionBoundary annotation not found");
        }
        Database.Datasource[] dss = anno.datasources();
        Map<String, Connection> map = connections.get();
        List<String> connsToClose = new ArrayList<String>(2);
        for (Database.Datasource d : dss) {
            String dsName = d.toString();
            Connection existingConn = map.get(dsName);
            if (existingConn == null) {
                DataSource dataSource = getDataSources().get(dsName);
                if (dataSource == null) {
                    throw new RuntimeException("DataSource requested by name " + dsName + " does not exist");
                }
                Connection newConn = dataSource.getConnection();
                map.put(dsName, newConn);
                connsToClose.add(dsName);
            } else {
                // Already have a connection for this datasource,
                // so this is a nested call
                if (LOG.isDebugEnabled()) {
                    LOG.debug("Found existing " + dsName + " connection for thread " + Thread.currentThread().getName());
                }
            }
        }

        try {
            return pjp.proceed();
        } finally {
            for (String c : connsToClose) {
                Connection conn = map.remove(c);
                try {
                    if (LOG.isDebugEnabled()) {
                        LOG.debug("Committing and closing " + c + " connection for thread " + Thread.currentThread().getName());
                    }
                    conn.commit();
                } finally {
                    getConnectionManager().releaseConnection(conn);
                }
            }
        }
    }

    /**
     * Intercept a method that returns a Connection from the turf sire data source,
     * and return the  current thread's connection instead.
     * @param pjp The join point
     * @return The annotated method's return value
     */
    public Object getTurfSireConnection(ProceedingJoinPoint pjp) {
        Map<String, Connection> map = connections.get();
        Connection conn = map.get(Database.Datasource.TURF_SIRE.toString());
        if (conn == null) {
            throw new RuntimeException("TransactionAspect.getTurfSireConnection initiated, but no TURF_SIRE connection found. " +
                    "Use @TransactionBoundary with TURF_SIRE datasource before requesting a TURF_SIRE database connection.");
        }
        if (LOG.isDebugEnabled()) {
            LOG.debug("Returning transaction TURF_SIRE conneciton for thread " + Thread.currentThread().getName());
        }
        return conn;
    }

    /**
     * Intercept a method that returns a Connection from the jockey data source,
     * and return the  current thread's connection instead.
     * @param pjp The join point
     * @return The annotated method's return value
     */
    public Object getJockeyConnection(ProceedingJoinPoint pjp) {
        Map<String, Connection> map = connections.get();
        Connection conn = map.get(Database.Datasource.JOCKEY.toString());
        if (conn == null) {
            throw new RuntimeException("TransactionAspect.getJockeyConnection initiated, but no JOCKEY connection found. " +
                    "Use @TransactionBoundary with JOCKEY datasource before requesting a JOCKEY database connection.");
        }
        if (LOG.isDebugEnabled()) {
            LOG.debug("Returning transaction JOCKEY conneciton for thread " + Thread.currentThread().getName());
        }
        return conn;
    }

    public ConnectionManager getConnectionManager() {
        return connectionManager;
    }

    public void setConnectionManager(ConnectionManager connectionManager) {
        this.connectionManager = connectionManager;
    }

    public Map<String, DataSource> getDataSources() {
        return dataSources;
    }

    public void setDataSources(Map<String, DataSource> dataSources) {
        this.dataSources = dataSources;
    }
}
